﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using elmah2.core.interfaces;
using elmah2.core.data;
using System.Configuration;
using elmah2.core.encoders;

namespace elmah2.core.loggers
{
  [Serializable]
  public abstract class DBLogger : IErrorLogger
  {
    protected List<Application> _apps;
    protected bool init = false;
    protected string _connectionString;
    protected string _name;
    
    public abstract IDbCommand GetErrorsCommand(int page, int numPerPage, int? app);
    public abstract IDbConnection GetConnection();
    public abstract IDbCommand GetCommand();

    public virtual string Name 
    {
      get { return _name; }
    }    

    public virtual QueryData LoadApplications()
    {
      IDbCommand command = GetCommand();
      command.CommandText = "SELECT * FROM Applications";
      return ExecuteCommandQuery(command);
    }

    public virtual ErrorData GetErrors(int page, int numPerPage, int? app)
    {
      ErrorData toReturn = new ErrorData();
      QueryData qd = ExecuteCommandQuery(GetErrorsCommand(page, numPerPage, app));
      if (qd.QuerySuccessful)
      {
        for (int i = 0; i < qd.RowCount; i++)
        {
          toReturn.TotalErrors = Convert.ToInt32(qd.GetDataForRowField(i, "Count"));
          toReturn.Errors.Add(new ErrorInfo()
          {
            ID = qd.GetDataForRowField(i, "ErrorID").ToString(),
            Code = qd.GetDataForRowField(i, "StatusCode").ToString(),
            Date = Convert.ToDateTime(qd.GetDataForRowField(i, "TimeUtc")).ToShortDateString(),
            Error = qd.GetDataForRowField(i, "Message").ToString(),
            Host = qd.GetDataForRowField(i, "Host").ToString(),
            Time = Convert.ToDateTime(qd.GetDataForRowField(i, "TimeUtc")).ToShortTimeString(),
            Type = qd.GetDataForRowField(i, "Type").ToString(),
            User = qd.GetDataForRowField(i, "UserName").ToString(),
            Application = GetApplication(Convert.ToInt32(qd.GetDataForRowField(i, "Application"))).Name
          });
        }
      }
      return toReturn;
    }

    public virtual bool AddApplication(string name)
    {
      List<Application> apps = GetApplications();
      Application found = apps.Where(R => R.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();

      if (found == null)
      {
        IDbCommand command = GetCommand();
        command.CommandText = "INSERT INTO Applications(ApplicationName) Values(@name)";
        AddParmToCommand(command, "@name", name);
        ExecuteCommand(command);
        _apps = null;
        return true;
      }
      return false;
    }

    public virtual Error GetError(string id)
    {
      Error toReturn = null;
      IDbCommand command = GetCommand();
      command.CommandText = "SELECT Details from Errors where ErrorId = @id";
      AddParmToCommand(command, "@id", id);
      QueryData qd = ExecuteCommandQuery(command);
      if (qd.QuerySuccessful)
      {
        toReturn = XmlEncoder.DecodeString(qd.GetDataForRowField(0, "Details").ToString());
      }

      return toReturn;
    }

    public virtual void LogError(Error error)
    {
      Application app = null;
      do
      {
        app = GetApplications().Where(R => R.Name.Equals(error.ApplicationName)).FirstOrDefault();
        if (app == null)
          AddApplication(error.ApplicationName);
      }
      while (app == null);


      string id = Guid.NewGuid().ToString();
      if (!init) throw new Exception("Please init before logging data");

      IDbCommand command = GetCommand();
      command.CommandText = "INSERT INTO Errors(ErrorID, Application, Source, Host, Type, Message, UserName, StatusCode, TimeUtc, Details) Values(@id, @app, @source, @host, @type, @msg, @user, @status, @time, @detail)";

      AddParmToCommand(command, "@id", id);
      AddParmToCommand(command, "@app", app.ID);
      AddParmToCommand(command, "@host", error.HostName);
      AddParmToCommand(command, "@source", error.Source);
      AddParmToCommand(command, "@type", error.Type);
      AddParmToCommand(command, "@msg", error.Message);
      AddParmToCommand(command, "@user", error.User);
      AddParmToCommand(command, "@status", error.StatusCode);
      AddParmToCommand(command, "@time", error.Time.ToUniversalTime());
      AddParmToCommand(command, "@detail", error.Encode());

      ExecuteCommand(command);
    }

    public virtual void AddParmToCommand(IDbCommand comamnd, string parm, object value)
    {
      IDbDataParameter newParm = comamnd.CreateParameter();
      newParm.Value = value;
      newParm.ParameterName = parm;
      comamnd.Parameters.Add(newParm);
    }

    public virtual void Init(Dictionary<string, string> config)
    {
      if (!init)
      {
        if (!config.ContainsKey("Name"))
          throw new Exception("A name is required");

        if (!config.ContainsKey("ConnectionString"))
          throw new Exception("Connection string target not defined not found");

        _name = config["Name"];
        try
        {
          _connectionString = ConfigurationManager.ConnectionStrings[config["ConnectionString"]].ConnectionString;
          init = true;
        }
        catch
        {
          throw new Exception("Connection string not found");
        }
      }
    }

    public Application GetApplication(int id)
    {
      return GetApplications().Where(R => R.ID == id).FirstOrDefault(); 
    }

    public List<Application> GetApplications()
    {
      if (_apps == null)
      {
        _apps = new List<Application>();
        QueryData dt = LoadApplications();

        for (int i = 0; i < dt.RowCount; i++)
        {
          _apps.Add(new Application()
              {
                ID = Convert.ToInt32(dt.GetDataForRowField(i, "ID")),
                Name = dt.GetDataForRowField(i, "ApplicationName").ToString(),
              });
        }
      }

      return _apps;
    }

    public void ExecuteCommand(IDbCommand command)
    {
      using (IDbConnection connection = GetConnection())
      {
        try
        {
          connection.Open();
          command.Connection = connection;
          command.ExecuteNonQuery();
        }
        catch(Exception e)
        {
          throw new Exception("Unable to access data store", e);
        }
      }
    }

    public QueryData ExecuteCommandQuery(IDbCommand command)
    {
      QueryData toReturn = new QueryData();
      using (IDbConnection connection = GetConnection())
      {
        try
        {
          connection.Open();
          command.Connection = connection;
          command.CommandTimeout = int.MaxValue;
          IDataReader reader = command.ExecuteReader();
          DataTable schema = reader.GetSchemaTable();

          if (schema != null)
          {
            for (int i = 0; i < schema.Rows.Count; i++)
            {
              toReturn.FieldMappings.Add(schema.Rows[i]["ColumnName"].ToString().ToLower(), i);
            }

            while (reader.Read())
            {
              object[] values = new object[reader.FieldCount];
              reader.GetValues(values);
              toReturn.RowData.Add(values);
            }
          }
          toReturn.QuerySuccessful = true;
        }
        catch(Exception e)
        {
          throw new Exception("Unable to query data store", e);
        }
        finally
        {
          command.Dispose();
        }
      }

      return toReturn;
    }
    
    public bool WriteOnly
    {
      get { return false; }
    }
  }
}
